using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace DesignPatterns.BuilderPattern.Core
{
    /**************************************************************************
    ** 
    **  This is an example dependency injection container. I tried to make it 
    **  very simialr to Ninject as that's my favorite DI container. 
    **  
    **  Below you'll find a Binding class to Register and hold the bindings, 
    **  (interface to implimentation), a Module class to modular configuration. 
    **  And the actualy object factory class which is what resolves bindings to 
    **  implementations. 
    ** 
    **  In the real world, you would always want to use and actualy framework. 
    **  This is something I threw together in about 30 minutes. :)
    **  
    **************************************************************************/
    
    /// <summary>
    /// Class to hold our dependency injection bindings
    /// </summary>
    public  class Binding
    {
        public Type Interface { get; set; }
        public Type Implementation { get; set; }
        public bool IsSingleton { get; set; }

        #region Fluent Methods
        public Binding To<TImplementation>()
        {
            Implementation = typeof (TImplementation);
            return this;
        }

        public Binding InSingletonScope()
        {
            IsSingleton = true;
            return this;
        }

        public Binding ToSelf() 
        {
            Implementation = Interface;
            return this;
        }
        #endregion
    }

    /// <summary>
    /// Class for module configuration
    /// </summary>
    public abstract class ObjectFactoryModule 
    {
        /// <summary>
        /// The method to implement by a module
        /// </summary>
        public abstract void Load();

        /// <summary>
        /// Holds the bindings
        /// </summary>
        public List<Binding> Bindings { get; set; }

        /// <summary>
        /// Registers a binding
        /// </summary>
        protected Binding Bind<TInterface>()
        {
            var binding = new Binding
            {
                Interface = typeof (TInterface)
            };
            Bindings.Add(binding);
            return binding;
        }

    }

    /// <summary>
    /// The actual dependency injection container
    /// </summary>
    public class ObjectFactory
    {
        private readonly List<Binding> _bindings = new List<Binding>();
        private readonly Dictionary<Type,object> _singletons = new Dictionary<Type, object>();
        
        /// <summary>
        /// Loads the passed in modules in the dependency injection container
        /// </summary>
        public ObjectFactory(params ObjectFactoryModule[] modules)
        {
            if (modules == null) return;
            foreach (var module in modules) 
            {
                module.Bindings = new List<Binding>();
                module.Load();
                _bindings.AddRange(module.Bindings);
            }
        }

        /// <summary>
        /// Resolves an abstract type to an implimentation
        /// </summary>
        public T Get<T>()
        {
            return (T)Get(typeof (T));
        }
        protected object Get(Type interfaceKey)
        {
            var binding = _bindings.LastOrDefault(i => i.Interface == interfaceKey);
            var o = Resolve(binding);
            return o;
        }

        /// <summary>
        /// Resolves an abstract type to an implimentation
        /// </summary>
        public IEnumerable<T> GetAll<T>()
        {
            return GetAll(typeof (T)) as IEnumerable<T>;
        }
        protected object GetAll(Type interfaceKey)
        {
            //Activator.CreateInstance isn't smart enought to unstand a list
            //of object have the same interface. So we have to create a list 
            //with the generic type (interface) specified. 
            var type = typeof (List<>).MakeGenericType(interfaceKey);
            var list = Activator.CreateInstance(type);

            //Get all bindings matching the interface
            var bindings = _bindings.Where(i => i.Interface == interfaceKey).ToList();
            bindings.ForEach(b =>
            {
                //Resolve the binding to an instance
                var instance = Resolve(b);

                //Add the instance to the list. 
                //This is the same as list.Add(instance);
                type.GetMethod("Add").Invoke(list, new[] { instance });
            });

            return list;
        }

        /// <summary>
        /// Resolves a binding to an implimentation
        /// </summary>
        protected object Resolve(Binding binding)
        {
            if (binding == null) return null;

            if (binding.IsSingleton) 
            {
                if (!_singletons.ContainsKey(binding.Interface)) {
                    var singleton = CreateInstance(binding.Implementation);
                    _singletons.Add(binding.Interface, singleton);
                }

                return _singletons.FirstOrDefault(i => i.Key == binding.Interface).Value;
            }

            var instance = CreateInstance(binding.Implementation);
            return instance;
        }

        /// <summary>
        /// Creates an instance
        /// </summary>
        private object CreateInstance(Type implementation)
        {
            var constructor = implementation.GetConstructors().First();
            
            var parameters = constructor.GetParameters();
            object[] parameterImplimentations = parameters.Select(p =>
            {
                var parameterType = p.ParameterType;
                var impl = parameterType.GetInterfaces().Any(i => i == typeof (IEnumerable))
                    ? GetAll(parameterType.GetGenericArguments().First())
                    : Get(parameterType);
                return impl;
            }).ToArray();

            var instance = constructor.Invoke(parameterImplimentations);
            return instance;
        }
    }
}
